import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Map;
import java.util.LinkedHashMap;

/**
 * Machine is the representation of the physical architecture of the 560
 * Machine. This class contains all of the registers, all 256 slots of memory,
 * file i/o capabilities, as well as the program counter, and the ability to
 * call all operations to change values of registers and memory contents. Having
 * one location for all these values and a class for each operation simplifies
 * the project, allowing for easy maitenance and debugging of the Machine.
 * 
 * @author Jonathan Hsu	
 * 
 * @created 8/6/11
 * 
 */
public class Machine {
	private int pcounter;
	private Word[] registers = new Word[4];
	private Word[] memory;
	private FileReader pin;
	private FileWriter pout, ptrace;
	private Operation[] operations = new Operation[16];

	public Machine(Word[] mem, int startpc, FileReader in, FileWriter out,
            FileWriter trace) {
		memory = mem;
		pcounter = startpc;
		pin = in;
		pout = out;
		ptrace = trace;

        // Initialize registers
        for (int i = 0; i < 4; i++) {
            registers[i] = new Word();
        }

		// Initialize operations
		operations[0] = new OpLoad(registers, memory, pin, pout);//
		operations[1] = new OpLoadI(registers, memory, pin, pout);//
		operations[2] = new OpStore(registers, memory, pin, pout);//
		operations[3] = new OpAdd(registers, memory, pin, pout);//
		operations[4] = new OpSub(registers, memory, pin, pout);//
		operations[5] = new OpMultiply(registers, memory, pin, pout);//
		operations[6] = new OpDivide(registers, memory, pin, pout);//
		operations[7] = new OpOr(registers, memory, pin, pout);
		operations[8] = new OpAnd(registers, memory, pin, pout);
		operations[9] = new OpSHL(registers, memory, pin, pout);
		operations[10] = new OpSHR(registers, memory, pin, pout);
		operations[11] = new OpIO(registers, memory, pin, pout);//
		operations[12] = new OpBR(registers, memory, pin, pout);//
		operations[13] = new OpBRZ(registers, memory, pin, pout);//
		operations[14] = new OpBRN(registers, memory, pin, pout);//
		operations[15] = new OpBRS(registers, memory, pin, pout);//
	}

	/**
	 * Run the machine from the beginning of the instruction input file. No
	 * location for first instruction is given, so it is assumed the program
	 * counter can start at position 1.
	 */
	public void run() {
		LinkedHashMap<String, Word> traceTable = new LinkedHashMap<String, Word>();
		traceAll();
        int i;
		for (i = 0; i < 200; i++) {
            int oldpcounter = pcounter;
			Word instr = memory[pcounter];
			if (instr.op() == 12 && instr.r() == 0) break;
            try {
                pcounter = operations[instr.op()].exec(pcounter, traceTable);
                trace(oldpcounter, traceTable);
                traceTable.clear();
            } catch (IOException e) {
                String err = "\nA fatal I/O error occurred. Program execution " +
                             "has bee terminated.\n";
                try {
                    pout.write(err);
                } catch (IOException e2) {
                    System.err.println("\nUnable to write to process output file.");
                    System.err.println(err);
                }
                traceTable.clear();
                traceTable.put(err, instr);
                trace(oldpcounter, traceTable);
                break;
            }
		}
        if (i == 200) {
            String err = "\nTime limit exceeded. More than 200 instructions have been attempted.\n";
            try {
                pout.write(err);
            } catch (IOException e) {
                System.err.println("\nUnable to write to process output file.");
                System.err.println(err);
            }
        }
		traceAll();
	}

	/**
	 * Run the machine from the beginning of the instruction input file.
	 * 
	 * @param loc
	 *            Location of the first instruction of the input file to be
	 *            executed.
	 */
	public void run(int loc) {
		pcounter = loc;
		run();
	}

	/**
	 * Trace the values of memory [0-255] as well as the registers of the
	 * machine before and after the execution of the operation requested in the
	 * input file.
	 * 
	 * @param instrloc
	 *            Location of the instruction the Machine executed
	 * @param traceTable
	 *            The String to 20 bit 'Word' table of before and after values
	 *            of memory and registers altered by the operation executed.
	 */
	public void trace(int instrloc, LinkedHashMap<String, Word> traceTable) {
		try {
            ptrace.write("Instruction " + memory[instrloc].toString() + 
                    " at Memory Location " +
                    String.format("%02x", instrloc).toUpperCase() +
                    " executed:\n");
            for (Map.Entry<String, Word> entry : traceTable.entrySet()) {
                ptrace.write(entry.getKey() + ": ");
                if (entry.getValue() != null) {
                    ptrace.write(entry.getValue().toString() + "\n");
                } else {
                    ptrace.write("\n");
                }
            }
            ptrace.write("\n");
		} catch (IOException e) {
			System.err.println("Could not write to trace file!");
		}
	}

    /**
	 * Trace all the values of memory [0-255] as well as the registers of the
	 * machine before and after the execution of the program.
	 */
	public void traceAll() {
        try {
            ptrace.write("Full trace of the machine's memory and registers:\n");
            for (int i = 0; i < 4; i++) {
                ptrace.write("Register #" + Integer.toString(i) + ": " +
                        registers[i].toString() + "\n");
            }
            for (int i = 0; i < 256; i++) {
                ptrace.write("Memory location " +
                        String.format("%02x", i).toUpperCase() + ": " +
                        memory[i].toString() + "\n");
            }
            ptrace.write("\n\n");
		} catch (IOException e) {
			System.err.println("Could not write to trace file!");
		}
	}
}
